-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BAAS-28253: Add optional tick tracking at the function level #115
Conversation
The performance impact of this change doesn't look too bad. (benchmark-vm) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sill need to look through all files, but posting some minor comments for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good idea to track functions. I have some questions about implementation.
runtime.go
Outdated
@@ -1513,7 +1522,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { | |||
vm.stash = &r.global.stash | |||
vm.sb = vm.sp - 1 | |||
} | |||
vm.prg = p | |||
vm.setPrg(p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q] We calculate the ticks before vm.runTry()
or vm.run()
where ticks is ticking(vm.r.ticks++
). Would it return 0 in some case? Correct me. I may be lost somewhere.
[q] can we add some tests to see if we're able to get the ticks metrics correctly?e.g. creating a vm to run a function and return this metrics. It may be not easy because we need to know the expected ticks(lol).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some background I had a previous iteration of this that tracked ticks at the exec call but that came with a ~20-30% performance hit.
} else {
// tick tracking occurred here
vm.prg.code[vm.pc].exec(vm)
}
[q] We calculate the ticks before vm.runTry() or vm.run()where ticks is ticking(vm.r.ticks++). Would it return 0 in some case? Correct me. I may be lost somewhere.
Yes there may be some scenarios on initial setup where this gets hit and no new ticks have been calculated yet. I think for the run and runTry scenario you are referencing it would probably get handled by the existing vm.prg being nil (i.e. this check in setPrg if vm.prg != nil {
)
I like your idea to add tests, I originally compared the setPrg solution against the results of the verbose solution I referenced above and did not see any discrepancies, but I'll add a few tests to solidify that check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q1] yes, I believe in some cases the ticks could be 0, but I believe that's fine. I'll let Calvin confirm that thought :)
[q2] agreed. Now that we're closer to a solution we should start adding some tests. We don't need to check exact ticks, but we should make sure that the metrics contain the correct functions and that the ticks of an expensive JS function are higher than the ticks of trivial functions. It might also be worth testing that executing a function X times generates X times more ticks than executing the same function once.
runtime.go
Outdated
@@ -1513,7 +1522,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { | |||
vm.stash = &r.global.stash | |||
vm.sb = vm.sp - 1 | |||
} | |||
vm.prg = p | |||
vm.setPrg(p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q1] yes, I believe in some cases the ticks could be 0, but I believe that's fine. I'll let Calvin confirm that thought :)
[q2] agreed. Now that we're closer to a solution we should start adding some tests. We don't need to check exact ticks, but we should make sure that the metrics contain the correct functions and that the ticks of an expensive JS function are higher than the ticks of trivial functions. It might also be worth testing that executing a function X times generates X times more ticks than executing the same function once.
vm.go
Outdated
func (vm *vm) setPrg(prg *Program) { | ||
if vm.r.functionTickTrackingEnabled { | ||
currentTicks := vm.r.Ticks() | ||
if vm.prg != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean by the situation of returning 0 is when vm.prg != nil. In an extreme case, in the very beginning, vm.r.Ticks()
should return 0 because ticks is not ticking yet. vm.lastFunctionTicks
is also 0. So line 328 will plus 0 at that time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think that edge case is ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I say that because I think I prefer the noop (adding 0) over the logic it would take to avoid that scenario.
Let me know if you disagree though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think this is fine, but @LijieZhang1998 let me know if you disagree/if I'm misunderstanding your comment. We don't care too much about these metrics being 100% accurate. We just want to figure out which are the top 10 or so functions we should be focusing on improving.
@@ -489,6 +498,8 @@ func (r *Runtime) init() { | |||
} | |||
r.vm.init() | |||
|
|||
r.tickMetrics = make(map[string]uint64) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to make this map get initialized per the functionTickTrackingEnabled setting but this actual would cause a problem (as demonstrated by the test). We need to have the vm object in order to enable the tick tracking but we also check if it is enabled when we create the vm object. So I think its better to just always initialize this map.
vm := New()
if tc.functionTickTrackingEnabled {
vm.EnableFunctionTickTracking()
}
(^ snippet from the unit test)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[opt] I would document this in code
runtime.go
Outdated
@@ -3229,3 +3243,11 @@ func (r *Runtime) getPrototypeFromCtor(newTarget, defCtor, defProto *Object) *Ob | |||
} | |||
return defProto | |||
} | |||
|
|||
func (self *Runtime) EnableFunctionTickTracking() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[opt] similar comment here about dropping the word "Function". Also, thoughts on grouping all the new tick functions together? i.e. move the TickMetrics
down here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the only reason I left "Function" here is "EnableTickTracking" sounds like it is for the main tick counter i.e. vm.r.ticks++... However we could do something like "EnableTickMetricTracking"
I agree on grouping these functions together. I'll do that.
vm.go
Outdated
func (vm *vm) setPrg(prg *Program) { | ||
if vm.r.functionTickTrackingEnabled { | ||
currentTicks := vm.r.Ticks() | ||
if vm.prg != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think this is fine, but @LijieZhang1998 let me know if you disagree/if I'm misunderstanding your comment. We don't care too much about these metrics being 100% accurate. We just want to figure out which are the top 10 or so functions we should be focusing on improving.
vm.go
Outdated
if vm.prg != nil { | ||
function := string(vm.prg.funcName) | ||
if vm.prg.src != nil { | ||
function = vm.prg.src.Name() + "_" + function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[mega-nit] function names could have an underscore. Thoughts on going with a period instead?
function = vm.prg.src.Name() + "_" + function | |
function = vm.prg.src.Name() + "." + function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason I went with a _ is because the prg.src.Name() can be a file name so test.js.functionName
looked a bit weird. I can go back to the period though, I don't have a strong opinion on this.
compiler_test.go
Outdated
@@ -156,6 +156,7 @@ func testLibX() *Program { | |||
|
|||
func (r *Runtime) testPrg(p *Program, expectedResult Value, t *testing.T) { | |||
vm := r.vm | |||
vm.profileTicks() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I'll remove, this is leftover from changing all of the vm.prg
sets to use the setPrg function.
func.go
Outdated
@@ -227,6 +227,7 @@ func (f *classFuncObject) _initFields(instance *Object) { | |||
if f.initFields != nil { | |||
vm := f.val.runtime.vm | |||
vm.pushCtx() | |||
vm.profileTicks() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q] could we call vm.profileTicks()
within vm.pushCtx()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a great idea! This handles a majority of the scenarios. There was a scenario that I left the vm.profileTicks() call but I'll comment on it after I push.
vm.go
Outdated
} | ||
|
||
type instruction interface { | ||
exec(*vm) | ||
} | ||
|
||
// profileTicks tracks the ticks for the current Program, this should be called prior to updating the program (i.e. vm.prg = p) | ||
func (vm *vm) profileTicks() { | ||
if vm.r.functionTickTrackingEnabled { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] let's flip this check and return early instead. That'll reduce indentation and improve the readability of this function. i.e.
if !vm.r.functionTickTrackingEnabled {
return
}
vm_test.go
Outdated
f() | ||
`, | ||
functionTickTrackingEnabled: true, | ||
expectedTickMetrics: map[string]uint64{"test.js_": 6, "test.js_f": 809}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these tests will break fairly easily if we compare the exact ticks. We don't really care about the tick values. We just want to make sure that the values are reasonable when compared to one another.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll just check to make sure that the keys line up and the values are greater than 0
}, | ||
} | ||
|
||
for _, tc := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q1]can we add some more tests such as executing executing class functions and multiple nested functions?
[q2] Also, do you mind sharing the metrics that we would generate from baas when using a dependency such as the aws sdk? The easiest way to do this might be to run our dependencies tests in evergreen with the tracking enabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q1]can we add some more tests such as executing executing class functions and multiple nested functions?
Yeah absolutely!
[q2] Also, do you mind sharing the metrics that we would generate from baas when using a dependency such as the aws sdk? The easiest way to do this might be to run our dependencies tests in evergreen with the tracking enabled.
Would that data be in splunk? Or would we need to parse the output of the tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Evergreen task with tick metric tracking hardcoded to true. Will update this thread with the results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'm curious to see if the current approach makes it easy enough for us to identify which function is consuming the most amount of this. Let me know when this is ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm for some reason the logs aren't showing the "tick metrics" logs for that evergreen run.
I think my local testing might alleviate some of your concerns though.
exports = async function(arg){
const { Buffer } = require('buffer');
const loremIpsumText = // long string
return Buffer.from(loremIpsumText);
};
[Note that the tickMetricsToLog array is sorted so we are looking at the top 5 tick contributors]
The majority of usage for this Buffer.from example is BufferJS::utf8ToBytes and BufferJS::blitBuffer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll also callout that these tick metrics are flat (not cumulative) in that we are measuring the specific time only for that function.
So in this case we would know that we would want to have a native implementation in Go for utf8ToBytes and probably blitBuffer as well, which is nice because this allows us to not have to potentially waste time porting over the entire Buffer.from implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm for some reason the logs aren't showing the "tick metrics" logs for that evergreen run.
I have a feeling this is happening because of how tick metrics are aggregated and published asynchronously. It doesn't even look like the cmd/server/main.go executes which is where this asynchronous logic would start.
I went ahead and set this up manually though, here are the results when running against aws-sdk-v3/s3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmac lol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh nice work, hello mr Buffer not surprised to see you up there!
@@ -1528,6 +1533,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { | |||
vm.clearStack() | |||
} else { | |||
vm.stack = nil | |||
vm.profileTicks() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without this we would miss the ticks tracked for the above vm.runTry
ex := vm.runTry(r.vm.ctx)
if ex == nil {
result = r.vm.result
} else {
err = ex
}
if recursive {
vm.popCtx()
vm.halt = false
vm.clearStack()
} else {
vm.stack = nil
vm.profileTicks()
vm.prg = nil
vm.setFuncName("")
r.leave()
}
It could turn out that this isn't necessary because it is captured by the pushCtx/restoreCtx, but I don't think it hurts to have this in place just to make sure we capturing any lingering ticks for the program. Worst case scenario is there are no new ticks and we add 0 for the program.
If we think this clutters the code I can dig more into this and see if it is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping it works for me.
@@ -1528,6 +1533,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { | |||
vm.clearStack() | |||
} else { | |||
vm.stack = nil | |||
vm.profileTicks() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping it works for me.
runtime.go
Outdated
func (self *Runtime) EnableTickMetricTracking() { | ||
self.tickMetricTrackingEnabled = true | ||
} | ||
|
||
func (self *Runtime) DisableTickMetricTracking() { | ||
self.tickMetricTrackingEnabled = false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add a comment for these exported functions?
vm_test.go
Outdated
f() | ||
`, | ||
tickMetricTrackingEnabled: true, | ||
expectedTickMetricsKeys: []string{"test.js.", "test.js.f"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[opt] Continuing this thread here.
ah good point... Yeah I don't have a strong opinion either. Maybe we can go with ::
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooo I really like ::
thanks!!
}, | ||
} | ||
|
||
for _, tc := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'm curious to see if the current approach makes it easy enough for us to identify which function is consuming the most amount of this. Let me know when this is ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 💯
@Calvinnix just to confirm, these benchmarks are with the ticks profiling enabled, right? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! pending this question
That is correct, that benchmark was from when the tick profiling was enabled by default and nothing material has changed with the logic since then. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work here, I just wanted one more test case and we're good to go!
@@ -489,6 +498,8 @@ func (r *Runtime) init() { | |||
} | |||
r.vm.init() | |||
|
|||
r.tickMetrics = make(map[string]uint64) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[opt] I would document this in code
@@ -594,3 +594,130 @@ func TestStashMemUsage(t *testing.T) { | |||
}) | |||
} | |||
} | |||
|
|||
func TestTickTracking(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] The one test I'm missing here is a function that throws an error. I was playing around with popCtx vs restoreCtx and most of the times profiling from either yields the same results. The only exception was when I was using it inside of the restoreCtx (like you are in this PR) where I noticed different ticks values when my function was throwing an error. Just thought it would be useful to have that case here. Realistically it will pass the test anyway and we won't see much different but profiling from popCtx only was missing out on some ticks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! I added a test to cover that we track ticks for a function that errors. 👍
No description provided.